import logging
import os
import re
import subprocess

import dnf
import dnf.exceptions
import dnf.module.module_base
import dnf.repo
import hawkey

from ocup.database import db
from ocup.dnf.download_progress import DnfDownloadCallback
from ocup.dnf.transaction_progress import DnfTransactionCallBack
from ocup.utils import is_efi, redirect_err, rm_rf

logger = logging.getLogger("ocup")


def get_base_object(installroot=None):
    base = dnf.Base()
    base.conf.gpgcheck = False
    base.conf.installroot = installroot
    if installroot and not os.path.isdir(installroot):
        os.makedirs(installroot)
    base.conf.sslverify = False
    base.conf.releasever = 9
    base.conf.module_platform_id = "platform:oc9.0"
    base.conf.protected_packages = []
    return base


class DnfManager(object):
    def __init__(self):
        self.base = None

    def close(self):
        if self.base:
            self.base.close()
            self.base = None

    def add_repo(self, load=True):
        repos = db.get("repos")
        if repos:
            for repo in repos:
                self._add_repo(repo["id"], repo["baseurl"], load=load)

    def _add_repo(self, repo_id, baseurl, load=True):
        repo_name = "ocup-{}".format(repo_id)
        repo = dnf.repo.Repo(repo_name, self.base.conf)
        repo.baseurl = [baseurl]

        repo.enable()
        self.base.repos.add(repo)
        if load:
            try:
                logger.info("Added '{}': {}", repo_name, baseurl)
                logger.info("Fetching {} metadata...".format(repo_name))
                repo.load()
            except dnf.exceptions.RepoError as e:
                logger.error("Error fetching metadata for {}: {}".format(repo_name, e))
                raise e

    def install(self, pkgs=None, local_pkgs=None):
        if pkgs:
            self.base.install_specs(pkgs)
        if local_pkgs:
            for pkg in self.base.add_remote_rpms(local_pkgs):
                try:
                    self.base.package_install(pkg, )
                except dnf.exceptions.MarkingError as e:
                    raise dnf.exceptions.Error(e)

    def do_install(self, pkgs=None, load=True):
        if pkgs is None:
            return
        self.base = get_base_object()
        self.add_repo(load)
        try:
            self.base.fill_sack()
        except dnf.exceptions.Error as e:
            logger.error("Failed to update metadata: {}".format(e))
            raise RuntimeError("Fetching metadata failed: {}".format(e))
        self.install(pkgs)
        self.do_transaction()
        self.close()

    def install_oc9_release(self):
        self._remove_oc8_release()
        self.do_install(["opencloudos-release", "opencloudos-repos"])

    def _remove_oc8_release(self):
        release_pkg = [
            "opencloudos-release",
            "epel-release",
        ]
        self.base = get_base_object()
        self.base.fill_sack(load_available_repos=False)
        q = self.base.sack.query()
        i = q.installed()
        for pkg in release_pkg:
            iq = i.filter(name=pkg)
            if iq.count() > 0:
                subprocess.run(["/usr/bin/rpm", "-e", "--nodeps", pkg])
        self.close()

    def remove_extras_pkg(self):
        self.base = get_base_object()
        self.base.fill_sack(load_available_repos=False)
        try:
            for pkg in self.base._do_package_lists("extras", []):
                if pkg.name not in ["ocup"]:
                    self.base.remove(pkg.name)
            self.do_transaction(allow_erasing=False)
        except Exception as e:
            logger.error(e)
        self.close()

    def upgrade(self):
        self._resolve_distro_conflict()
        self._delete_conflict_files()
        self._distro_sync()

    def _distro_sync(self):
        self.base = get_base_object()
        self.add_repo(load=False)
        try:
            self.base.fill_sack(load_system_repo=True)
        except dnf.exceptions.Error as e:
            logger.error("Failed to update metadata: {}".format(e))
            raise RuntimeError("Fetching metadata failed: {}".format(e))
        removes = db.get("removes")
        for i in removes:
            self.base.remove(i)
        installs = db.get("installs")
        for i in installs:
            self.base.install(i)
        self._group_update()
        self.base.distro_sync()
        self.do_transaction()
        self.close()

    def _resolve_distro_conflict(self):
        self.base = get_base_object()
        self.add_repo(load=False)
        self.base.fill_sack(load_system_repo=True)
        self._group_update()
        self.base.distro_sync()
        try:
            self.do_transaction()
        except dnf.exceptions.Error as e:
            # 需要安装的包
            installs = set()
            # 需要卸载的包
            removes = set()
            # 需要删除的冲突文件
            deletes = set()
            conflict_re = '\s+file (\S+) from install of (\S+) conflicts with file from package (\S+)'
            for err_line in str(e).splitlines()[1:]:
                match = re.match(conflict_re, err_line)
                if match:
                    new_pkg = self._get_rpm_name(match.group(2))
                    old_pkg = self._get_rpm_name(match.group(3))
                    if new_pkg == old_pkg:
                        deletes.add(match.group(1))
                    else:
                        installs.add(match.group(2))
                        removes.add(match.group(3))
                else:
                    raise e
            if is_efi():
                installs.add("shim")
            installs = list(installs)
            removes = list(removes)
            deletes = list(deletes)
            db.set("installs", installs)
            db.set("removes", removes)
            db.set("deletes", deletes)
        self.close()

    def _group_update(self):
        self.base.read_comps()
        install_groups = [g.id for g in self.base.comps.groups if self.base.history.group.get(g.id)]
        if install_groups:
            self.base.env_group_upgrade(install_groups)
        install_environments = [g.id for g in self.base.comps.environments if self.base.history.env.get(g.id)]
        if install_environments:
            self.base.env_group_upgrade(install_environments)

    @staticmethod
    def disable_modules():
        module_list = os.listdir("/etc/dnf/modules.d")
        for module_config in module_list:
            file_path = os.path.join("/etc/dnf/modules.d", module_config)
            if os.path.isfile(file_path):
                try:
                    os.remove(file_path)
                except IOError as e:
                    logger.error(e)

    @staticmethod
    def _get_rpm_name(full_name):
        try:
            name = hawkey.split_nevra(full_name).name
        except hawkey.ValueException as e:
            logger.warning(e)
            name = full_name
        return name

    @staticmethod
    def _delete_conflict_files():
        logger.info("delete conflict files")
        deletes = db.get("deletes")
        if deletes:
            for delete in deletes:
                try:
                    rm_rf(delete)
                except IOError as e:
                    logger.error(e)

    def do_transaction(self, allow_erasing=True):
        logger.info("Resolving transaction...", )
        with redirect_err():
            self.base.resolve(allow_erasing=allow_erasing)
        logger.info("Downloading packages...")
        self.base.download_packages(self.base.transaction.install_set, DnfDownloadCallback())
        logger.info("Installing...")
        try:
            with redirect_err():
                self.base.do_transaction(display=DnfTransactionCallBack())
        except dnf.exceptions.Error as e:
            err_line = str(e)
            is_err = False
            install_re = '(?:\s*package.* is already installed\s*|\s*Transaction test error\s*)'
            for l in err_line.splitlines():
                if not re.match(install_re, l):
                    is_err = True
                    break
            if is_err:
                raise e

    @staticmethod
    def clean_cache():
        logger.debug("clean cache")
        try:
            subprocess.run(["/usr/bin/rm", "-rf", "/var/cache/dnf/*"])
        except Exception as e:
            logger.error(e)
            raise e
